create-node-template
Version:
Create node.js/express boilerplate with one command
381 lines (380 loc) • 15.2 kB
JavaScript
import { cyan, green, red, yellow, bold } from '../../utils/index.js';
// import { cyan, green, red, yellow, bold, blue } from 'picocolors';
import { Command } from 'commander';
import Conf from 'conf';
import checkForUpdate from 'update-check';
import prompts from 'prompts';
import { getPkgManager } from './helpers/get-pkg-manager.js';
import { isFolderEmpty } from './helpers/is-folder-empty.js';
import { validateNpmName } from './helpers/validate-pkg.js';
import packageJson from '../../../package.json';
// import { createApp, DownloadError } from './create-app';
// import ciInfo from 'ci-info';
import fs from 'fs';
import path from 'path';
let projectPath = '';
const handleSigTerm = () => process.exit(0);
process.on('SIGINT', handleSigTerm);
process.on('SIGTERM', handleSigTerm);
const onPromptState = (state) => {
if (state.aborted) {
// If we don't re-enable the terminal cursor before exiting
// the program, the cursor will remain hidden
process.stdout.write('\x1B[?25h');
process.stdout.write('\n');
process.exit(1);
}
};
const formatDesc = (description) => {
return `
${description}
`;
};
const program = new Command(packageJson.name)
.version(packageJson.version)
.arguments('<project-directory>')
.usage(`${green('<project-directory>')} [options]`)
.action((name) => {
projectPath = name;
})
.option('--eslint', formatDesc('Initialize with eslint config.'))
.option('--import-alias <alias-to-configure>', formatDesc('Specify import alias to use (default "@/*").'))
.option('--use-npm', formatDesc('Explicitly tell the CLI to bootstrap the application using npm'))
.option('--use-pnpm', formatDesc('Explicitly tell the CLI to bootstrap the application using pnpm'))
.option('--use-yarn', formatDesc('Explicitly tell the CLI to bootstrap the application using Yarn'))
.option('--use-bun', formatDesc('Explicitly tell the CLI to bootstrap the application using Bun'))
.option('-t, --template [name]', formatDesc('Which template to bootstrap the app with. You can use any of:\n' +
' - node-basic: A basic Node.js app.\n' +
' - express-basic: A basic Express.js app.\n' +
' - express-advanced: An advanced Express.js app with ready for production.'))
.option('--reset-preferences', formatDesc('Explicitly tell the CLI to reset any stored preferences'))
.allowUnknownOption()
.parse(process.argv);
const optionsS = program.opts();
// const packageManager = 'npm';
const packageManager = optionsS.useNpm
? 'npm'
: optionsS.usePnpm
? 'pnpm'
: optionsS.useYarn
? 'yarn'
: optionsS.useBun
? 'bun'
: getPkgManager();
// const packageManager = !!program.useNpm
// ? 'npm'
// : !!program.usePnpm
// ? 'pnpm'
// : !!program.useYarn
// ? 'yarn'
// : !!program.useBun
// ? 'bun'
// : getPkgManager();
async function run() {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call
/* negate the above */
const conf = new Conf({ projectName: 'create-node-template' });
// if (optionsS.resetPreferences) {
// conf.clear();
// console.log(`Preferences reset successfully`);
// return;
// }
if (typeof projectPath === 'string') {
projectPath = projectPath.trim();
}
if (!projectPath) {
const res = await prompts({
onState: onPromptState,
type: 'text',
name: 'path',
message: 'What is your project named?',
initial: 'my-app',
validate: (name) => {
const validation = validateNpmName(path.basename(path.resolve(name)));
if (validation.valid) {
return true;
}
return 'Invalid project name: ' + validation.problems[0];
},
});
if (typeof res.path === 'string') {
projectPath = res.path.trim();
}
}
if (!projectPath) {
console.log('\nPlease specify the project directory:\n' +
` ${cyan(program.name())} ${green('<project-directory>')}\n` +
'For example:\n' +
` ${cyan(program.name())} ${green('my-next-app')}\n\n` +
`Run ${cyan(`${program.name()} --help`)} to see all options.`);
process.exit(1);
}
const resolvedProjectPath = path.resolve(projectPath);
const projectName = path.basename(resolvedProjectPath);
const validation = validateNpmName(projectName);
if (!validation.valid) {
console.error(`Could not create a project called ${red(`"${projectName}"`)} because of npm naming restrictions:`);
validation.problems.forEach(p => console.error(` ${red(bold('*'))} ${p}`));
process.exit(1);
}
// if (program.example === true) {
// console.error('Please provide a template name, otherwise remove the template option.');
// process.exit(1);
// }
// if (program.example === true) {
// console.error(
// 'Please provide an example name or url, otherwise remove the example option.'
// )
// process.exit(1)
// }
/**
* Verify the project dir is empty or doesn't exist
*/
const root = path.resolve(resolvedProjectPath);
const appName = path.basename(root);
const folderExists = fs.existsSync(root);
if (folderExists && !isFolderEmpty(root, appName)) {
process.exit(1);
}
const template = typeof optionsS.template === 'string' && optionsS.template.trim();
const preferences = (conf.get('preferences') || {});
/**
* If the user does not provide the necessary flags, prompt them for whether
* to use TS or JS.
*/
if (!template) {
const defaults = {
typescript: true,
eslint: true,
tailwind: true,
app: true,
srcDir: false,
importAlias: '@/*',
customizeImportAlias: false,
};
const getPrefOrDefault = (field) => preferences[field] ?? defaults[field];
// if (!program.typescript && !program.javascript) {
// if (ciInfo.isCI) {
// // default to TypeScript in CI as we can't prompt to
// // prevent breaking setup flows
// program.typescript = getPrefOrDefault('typescript');
// } else {
// const styledTypeScript = blue('TypeScript');
// const { typescript } = await prompts(
// {
// type: 'toggle',
// name: 'typescript',
// message: `Would you like to use ${styledTypeScript}?`,
// initial: getPrefOrDefault('typescript'),
// active: 'Yes',
// inactive: 'No',
// },
// {
// /**
// * User inputs Ctrl+C or Ctrl+D to exit the prompt. We should close the
// * process and not write to the file system.
// */
// onCancel: () => {
// console.error('Exiting.');
// process.exit(1);
// },
// }
// );
// /**
// * Depending on the prompt response, set the appropriate program flags.
// */
// program.typescript = Boolean(typescript);
// program.javascript = !Boolean(typescript);
// preferences.typescript = Boolean(typescript);
// }
// }
// if (!process.argv.includes('--eslint') && !process.argv.includes('--no-eslint')) {
// if (ciInfo.isCI) {
// program.eslint = getPrefOrDefault('eslint');
// } else {
// const styledEslint = blue('ESLint');
// const { eslint } = await prompts({
// onState: onPromptState,
// type: 'toggle',
// name: 'eslint',
// message: `Would you like to use ${styledEslint}?`,
// initial: getPrefOrDefault('eslint'),
// active: 'Yes',
// inactive: 'No',
// });
// program.eslint = Boolean(eslint);
// preferences.eslint = Boolean(eslint);
// }
// }
// if (!process.argv.includes('--tailwind') && !process.argv.includes('--no-tailwind')) {
// if (ciInfo.isCI) {
// program.tailwind = getPrefOrDefault('tailwind');
// } else {
// const tw = blue('Tailwind CSS');
// const { tailwind } = await prompts({
// onState: onPromptState,
// type: 'toggle',
// name: 'tailwind',
// message: `Would you like to use ${tw}?`,
// initial: getPrefOrDefault('tailwind'),
// active: 'Yes',
// inactive: 'No',
// });
// program.tailwind = Boolean(tailwind);
// preferences.tailwind = Boolean(tailwind);
// }
// }
// if (!process.argv.includes('--src-dir') && !process.argv.includes('--no-src-dir')) {
// if (ciInfo.isCI) {
// program.srcDir = getPrefOrDefault('srcDir');
// } else {
// const styledSrcDir = blue('`src/` directory');
// const { srcDir } = await prompts({
// onState: onPromptState,
// type: 'toggle',
// name: 'srcDir',
// message: `Would you like to use ${styledSrcDir}?`,
// initial: getPrefOrDefault('srcDir'),
// active: 'Yes',
// inactive: 'No',
// });
// program.srcDir = Boolean(srcDir);
// preferences.srcDir = Boolean(srcDir);
// }
// }
// if (!process.argv.includes('--app') && !process.argv.includes('--no-app')) {
// if (ciInfo.isCI) {
// program.app = getPrefOrDefault('app');
// } else {
// const styledAppDir = blue('App Router');
// const { appRouter } = await prompts({
// onState: onPromptState,
// type: 'toggle',
// name: 'appRouter',
// message: `Would you like to use ${styledAppDir}? (recommended)`,
// initial: getPrefOrDefault('app'),
// active: 'Yes',
// inactive: 'No',
// });
// program.app = Boolean(appRouter);
// }
// }
// if (typeof program.importAlias !== 'string' || !program.importAlias.length) {
// if (ciInfo.isCI) {
// // We don't use preferences here because the default value is @/* regardless of existing preferences
// program.importAlias = defaults.importAlias;
// } else if (process.argv.includes('--no-import-alias')) {
// program.importAlias = defaults.importAlias;
// } else {
// const styledImportAlias = blue('import alias');
// const { customizeImportAlias } = await prompts({
// onState: onPromptState,
// type: 'toggle',
// name: 'customizeImportAlias',
// message: `Would you like to customize the default ${styledImportAlias} (${defaults.importAlias})?`,
// initial: getPrefOrDefault('customizeImportAlias'),
// active: 'Yes',
// inactive: 'No',
// });
// if (!customizeImportAlias) {
// // We don't use preferences here because the default value is @/* regardless of existing preferences
// program.importAlias = defaults.importAlias;
// } else {
// const { importAlias } = await prompts({
// onState: onPromptState,
// type: 'text',
// name: 'importAlias',
// message: `What ${styledImportAlias} would you like configured?`,
// initial: getPrefOrDefault('importAlias'),
// validate: value =>
// /.+\/\*/.test(value) ? true : 'Import alias must follow the pattern <prefix>/*',
// });
// program.importAlias = importAlias;
// preferences.importAlias = importAlias;
// }
// }
// }
}
// try {
// await createApp({
// appPath: resolvedProjectPath,
// packageManager,
// // example: example && example !== 'default' ? example : undefined,
// // examplePath: program.examplePath,
// typescript: program.typescript,
// tailwind: program.tailwind,
// eslint: program.eslint,
// appRouter: program.app,
// srcDir: program.srcDir,
// importAlias: program.importAlias,
// });
// } catch (reason) {
// if (!(reason instanceof DownloadError)) {
// throw reason;
// }
// const res = await prompts({
// onState: onPromptState,
// type: 'confirm',
// name: 'builtin',
// message:
// `Could not download "${example}" because of a connectivity issue between your machine and GitHub.\n` +
// `Do you want to use the default template instead?`,
// initial: true,
// });
// if (!res.builtin) {
// throw reason;
// }
// await createApp({
// appPath: resolvedProjectPath,
// packageManager,
// typescript: program.typescript,
// eslint: program.eslint,
// tailwind: program.tailwind,
// appRouter: program.app,
// srcDir: program.srcDir,
// importAlias: program.importAlias,
// });
// }
conf.set('preferences', preferences);
}
const update = checkForUpdate.default(packageJson).catch(() => null);
async function notifyUpdate() {
try {
const res = await update;
if (res?.latest) {
const updateMessage = packageManager === 'yarn'
? 'yarn global add create-next-app'
: packageManager === 'pnpm'
? 'pnpm add -g create-next-app'
: packageManager === 'bun'
? 'bun add -g create-next-app'
: 'npm i -g create-next-app';
console.log(yellow(bold('A new version of `create-next-app` is available!')) +
'\n' +
'You can update by running: ' +
cyan(updateMessage) +
'\n');
}
process.exit();
}
catch {
// ignore error
}
}
run()
.then(notifyUpdate)
.catch(async (reason) => {
console.log();
console.log('Aborting installation.');
if (reason.command) {
console.log(` ${cyan(reason.command)} has failed.`);
}
else {
console.log(red('Unexpected error. Please report it as a bug:') + '\n', reason);
}
console.log();
await notifyUpdate();
process.exit(1);
});
//# sourceMappingURL=program%5BOLD%5D.js.map